# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
## NPSolitaire:
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## from PySolFC code:
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Game class with mixins that creates, manages and records a solitaire card game.
"""
__docformat__ = 'restructuredtext en'
#_ = lambda n: n

import logging

LOGGER = logging.getLogger('PySolFC.GameLib')

from ..animation import animate_redeal
from ..pysolaudio import AUDIO_DEV
from ..settings import TITLE, CONF_OPT
from ..statdb import STAT_DB
from ..utillib.misc import Timer as GameTimer, Size, Scale
from ..viewlib.console import update_console
from ..viewlib.constants import CURSOR_WATCH
from . import state
from .info import GT
from .bind import GameBind
from .demo import GameDemo
from .drag import GameDrag
from .move import GameMove
from .pack import GamePack

TIMER_UPDATE = 200


class Game(GamePack, GameMove, GameDemo, GameDrag, GameBind):
    """
    Base class for all solitaire games
    """
    __VERSION__ = 7.70
    _can_save_ = True
    filename = ''

    def __init__(self, gameinfo):
        """
        Initialize game details.

        Set initial state for attributes. Call next method in MRO.

        *Parameters*:
          gameinfo : {Map}
            gameinfo data from GAME_DB set by Game details itselfouter App loop.

        *Variables*:
          self.stackmap : {dict}
            Empty map of id int value at coords tuple key set by
            *Stack.__init__()* to locate by location.
          self.allstacks : {list}
            Empty list of *Stack* set by *Stack.__init__()* to locate by id.
          self.s : {Map}
            Empty map of common stacks type keys -- talon, waste, foundations,
            rows, reserves, xwastes, braid, internals and their stack object /
            object list values. Set during create_game().
          self.sg : {Mapping}
            Empty map of common stacks groups for drag stack, hint moves, auto
            play, amd stack highlight. Set during create_game() and adjusted
            during _pile_prepare().
        """
        self.__info__ = gameinfo
        super(Game, self).__init__()
        self.stackmap = {}
        self.allstacks = []
        # common stack types
        self.s = dict(talon=None, wastes=[], foundations=[], rows=[],
                        reserves=[], braid=[], internals=[])
        # common stack groups
        self.sg = {'openstacks': [],  # buildable piles with get_closest_stack()
                   # hint.AbstractHint based hints discover new moves
                   'Talons': [], 'reservestacks': [],
                   # hint.AbstractHint based hints discover new moves
                   # and _autopiles for flip, drop, and quickplay
                   'dropstacks': [],
                   # _hightlight_piles property for piles to highlight
                   'hp_stacks': []}
        self.top = self.nextgame = self.canvas = None
        self.win_animation = None
        self.__init_view_size = Size(0, 0)
        self.__init_text_coords = {}
        self._timer = GameTimer()
        self.is_preview = self.busy = self.is_pause = False
        self.play_timer = None

    def __getitem__(self, key):
        """
        Get Game Details by key.

        Mirror Game detail functionality.

        .. Game[key] -> *
        """
        return self.__info__.get(key, None)

    @property
    def width(self):
        """
        Return inital table width for game.

        *note:* This is inital table width required by layout not window size.

        .. width -> int
        """
        return self.__init_view_size.w

    @property
    def height(self):
        """
        Return inital table height for game.

        *note:* This is inital table height required by layout not window size.

        .. height -> int
        """
        return self.__init_view_size.h

    @property
    def table_size(self):
        """
        Return current table size for game.

        *note:* This is scaled table size required by layout not window size.

        .. table_size -> Size
        """
        scale_ = Scale(*CONF_OPT['scale'])
        return self.__init_view_size._scale(scale_)

    @table_size.setter
    def table_size(self, value):
        """
        Set inital table size for game.

        Given value should be a Size tuple of width and height that includes
        entire visable layout. It should be called by _create_game().

        *note:* This is intial table size required by layout not window size.

        .. table_size = Size

        *Parameters*:
          value : {Size}
        """
        self.__init_view_size = Size(int(value[0]), int(value[1]))

    def reset(self, restart=False):
        """
        Reset current game.

        Call next method in order. Reset timer and win animation. Called by
        new and restore methods.

        .. reset([restart:bool=False]):

        *Keywords*:
          restart : {bool}
            Allow some attributes to survive a reset, but not a restart.
        """
        ancestor = super(Game, self)
        if hasattr(ancestor, 'reset'):
            ancestor.reset(restart)
        self.filename = ''
        self._timer(restart)
        if not restart:
            self.win_animation = None  # reset main win animation object

    @state.game_busy
    def _pile_prepare(self):
        """
        Prepare stack data for game.

        Confirm pile goup details. Sequences in *s* and *sg* are optimized as
        tuples. Each pile in allstacks sequene has their prepare and assert
        method called. Confirm talon has rounds and max_rounds attrs.

        .. _pile_prepare() -> None

        *Variables*:
          self.allstacks : {tuple}
            Optimize listing for speed with tuple.
          self.s : {Maping}
            Optimize each value listing for speed with tuples.
          self.sg['openstacks'] : {tuple}
            Confirm Stack listing can take cards.
          self.sg['hpstacks'] : {tuple}
            Confirm Stack listing sg['dropstacks'] can move cards.

        *Raise Error*:
          AssertionErrors for any stack that fails their assertStack() method
          and talon lacks required attrs.
        """
        pile, group = self.s, self.sg
        group['openstacks'] = tuple(p for p in group['openstacks']
                                  if p.cap['max_accept'] >= p.cap['min_accept'])
        group['hp_stacks'] = tuple(p for p in group['dropstacks']
                                     if p.cap['max_move'] >= 2)
        # convert stackgroups to tuples (speed)
        self.allstacks = tuple(self.allstacks)
        for key in ('wastes', 'foundations', 'rows', 'reserves', 'braid',
                    'internals'):
            assert isinstance(pile[key], (list, tuple))
            pile[key] = tuple(pile[key])
        for key in ('Talons', 'dropstacks', 'reservestacks'):
            assert isinstance(group[key], (list, tuple))
            pile[key] = tuple(group[key])
        # init the stack view
        for cpile in self.allstacks:
            cpile.prepareStack()
            cpile.assertStack()
        if pile['talon']:
            assert all(hasattr(pile['talon'], t)
                       for t in ('round', 'max_rounds',))

    @state.game_busy
    def prepare(self, app, preview=False):
        """
        Set app attributes on game. Call next method in order. Init text
        mappings for canvas text. Create layout and card/tile pack(s) method.
        Prepare and optimize piles and their regions. When not preview set
        additional key/mouse binding, prepare canvas size and reset status help.

       *Parameters*:
          app : {Application}
            game application with outer loop and persistent variables.
          preview : {bool} False
           game is within a preview dialog.

        *Variables*:
          self.app : {PySolApp}
            Set to app value passed.
          self.top : {MfxRoot}
            Set to app.top value.
          self.canvas : {ScrolledCanvas}
            Set to app.canvas value.
          self.drag : {Mapping}
            Initialize values for drag stack between stacks.
          self.texts : {Map}
            Initialize values.

        *Notes*:
          Merged common and preview methods into single method
        """
        LOGGER.info('gamename: %s', self['name'])
        self.app = app
        self.top = app.top
        self.nextgame = app.nextgame
        self.canvas = app.canvas
        self.is_preview = preview

        progress = self.app.progress

        ancestor = super(Game, self)
        if hasattr(ancestor, 'prepare'):
            ancestor.prepare(progress)

        self.filename = ''
        self.drag = dict(
            event=None,               # current event
            timer=None,               # current event timer
            start_x=0,                # X coord of initial drag event
            start_y=0,                # Y coord of initial drag event
            stack=None,
            cards=[],
            index=-1,
            shadows=[],               # list of canvas images
            shade_stack=None,         # stack currently shaded
            shade_img=None,           # canvas image
            canshade_stacks=[],       # list of stacks already tested
            noshade_stacks=[],        # for this drag
        )
        # optional canvas.TextItems
        self.texts = {
            'info': None,                # misc info text
            'help': None,                # a static help text
            'misc': None,
            'score': None,               # for displaying the score
            'base_rank': None,           # for displaying the base_rank
            'list': [], }                 # list of additional texts
        # initial position of the texts
        self.__init_text_coords = self.texts.copy()
        if not preview:
            self.top.wm_set_cursor(cursor=CURSOR_WATCH)
            self.top.wm_title(TITLE + ' - ' + self['name'])
            self.top.wm_iconname(TITLE + ' - ' + self['name'])

        # create the game
        if progress:
            progress.update(step=1)

        self._create_game()
        self._pack_create(progress=progress)

        # set some defaults
        self._pile_prepare()
        self._optimize_regions()
        if not preview:
            self.init_bindings()
            self._view_prepare()
            update_console(gamenumber='', moves='', stats='', info='', time='',
                           assist='')
        else:
            self.canvas.set_tablesize(self.table_size)

    @state.game_busy
    def new(self, random=None, restart=False):
        """
        Start a new card game.

        .. new(random:Random=None, restart:bool=False)
             -> None

        *Parameters*:
          random : {Random}
            random object subclass of PysolRandom.
          restart : {bool}
            game reset method. Default False.

        *Variables*:
          self.move_state : {int}
            set S_INIT state
        """
        self.stats_update('restart' if restart else '')
        # 1) Clean up and set inital state
        random = self.random if restart else random
        self.top.wm_set_cursor(cursor=CURSOR_WATCH)
        if self.win_animation is not None:
            self.win_animation.stop()
        if not self.is_preview:
            self.top.event_generate('<<reset_menus>>')
        animate_redeal(self)
        self.reset(restart=restart)
        # 2) Shuffled cards and add to Talon
        self.do_talon_init(random_class=random)
        # 3) Ensure toplevel unhid
        if not self.is_preview:
            self.top.wm_map()
            self.top.update()
        AUDIO_DEV.stop()
        # 4) Deal initial cards to stacks and perform sanity checks
        self.move_state = self.S_INIT
        if CONF_OPT['animate_speed'] > 0:
            self.canvas.update_idletasks()
        if self.can_play_sound:
            AUDIO_DEV.play('deal')
        self._start()
        if self['si']['game_flags'] & GT.OPEN:
            if self.s['talon']:
                assert len(self.s['talon'].cards) == 0
            for pile in (p for p in self.allstacks if p.is_visible):
                for card in pile.cards:
                    assert card.face_up
        self._move_reset()
        # 5) Update interface
        self._view_update()
        # 6) Begin game play
        AUDIO_DEV.stop()
        self.auto_play()
        self.top.wm_set_cursor()
        self._timer_start()

    @state.game_busy
    def restore(self, data, reset=True):
        """
        Restore prior game details.

        Optionally call reset method, before calling next method in order. Set
        finished state. Update view for restored game then start timer.

        .. restore(game:GameData[, reset:bool=True, autoplay:bool=True]) -> None

        *Parameters*:
          data: {GameData}
            Data to restore game details.
          reset: {boolean}
            Reset current game before restore.

        *Variables*:
          finished :
            set to finished of given game
        """
        self.stats_update()
        self.top.wm_set_cursor(cursor=CURSOR_WATCH)
        if reset:
            self.reset()
        if data['update_flag'] in [0, 1]:
            self.nextgame.update(data['nextgame'])
            self._timer = data['timer']
        del data['nextgame']
        del data['timer']
        ancestor = super(Game, self)
        if hasattr(ancestor, 'restore'):
            ancestor.restore(data, reset)

        # 3) Update interface
        self._view_update()
        self.top.after(200, self._config_handler)  # wait for canvas
        if not self.is_preview:
            self.top.wm_map()
            self.top.update()
        # 4) Begin game play
        self.auto_play()
        self.top.wm_set_cursor()
        self._timer_start()

    def nextgame_flags(self, id_, random_class=None):
        """
        Check differences between current and next card game.

        *Parameters*:
          id\_ : {int}
            next game id be loaded

        *Keywords*:
          random_class : {Random}
            random object subclass of *PysolRandom*.

        *Returns*:
          game change differences with id, cardset, seed as int value.
            0 - no change; 1 - id change; 2 - cardset change;
            3 - id and cardset change; 16 - seed change;
            17 - id and seed change; 18 - cardset and seed change
            19 - id, cardset and seed change
        """
        flags = 0
        ancestor = super(Game, self)
        if hasattr(ancestor, 'nextgame_flags'):
            flags = ancestor.nextgame_flags(id_, random_class=random_class)
        if id_ != self['id']:
            flags = flags | 1
        return flags

    @state.not_state('is_preview')
    def quit(self, id_=0, seed=None, data=None, mode=None):
        """
        Quit to outer mainloop.

        .. quit([id_:int=0, random:Random, data:GameData, mode:str]) -> None

        Update stats. Set given values to nextgame map. Pass control to outer
        loop. Depending on map another game may start next or app may quit too.

        Keywords:
          id\_ : {int} 0 (no id)
            Load different game id next game.
          random : {Random} None (new seed)
            Load this random object next game.
          data : {GameData} None
            Load this dumped game next game.
          mode : {str} None ['hold'|'load'|'mark'|'demo'|None]
            mode for outerloop.

        *Variables*:
          self.nextgame : {Map}
            update id, random, data, mode, with passed values.
        """
        self.demo_stop()
        if not self.is_finish:
            self.log()
        self.stats_update('hold' if mode in ['hold', 'mark'] else '')
        self.nextgame['id'] = id_
        self.nextgame['random'] = seed
        self.nextgame['data'] = data
        self.nextgame['mode'] = mode
        update_console(gamenumber='', moves='', stats='', info='', time='',
                       assist='')
        self.top.quit()

    @state.not_state('is_preview')
    def stats_update(self, mode=None):
        """
        Update stats for current game.

        Resume from pause. Update stats when mode given is not hold and restart.
        When mode is restart update nextgame restart counter.

        *note:* This should be called before new, restore, and quit methods.

        .. stats_update(self, mode=None):

        *Parameters:*
          mode : {str[restart|None]}
            next game will start this game over.

        *Variables*:
          self.nextgame['counter']['restart'] : int
            increment value by 1
        """
        self._timer_pause(False)  # Unpause game
        self.top.wm_save_state()
        if mode is None:
            return
        if mode == 'restart':
            if self.move_index > 0 and self.move_player > 0:
                self.nextgame['counter']['restart'] += 1
            return
        if self.changed() and CONF_OPT['update_stats']:
            STAT_DB.update(CONF_OPT['player'], self, status=self.is_success)

    @state.canvas_busy
    def _view_prepare(self):
        """
        Prepare main canvas for possible resize.

        Restore table size. Store initial text coords. Call resize view method.

        .. \_view_prepare():
        """
        # Restore canvas geometry
        if CONF_OPT['scale_auto']:
            self.canvas.set_tablesize(CONF_OPT['wm_geometry'])
        else:
            self.canvas.set_tablesize(self.table_size)
            self.top.wm_geometry('')
        # Preserve initial position for self.text items.
        for key, value in self.texts.items():
            if isinstance(value, (list, tuple,)):
                self.__init_text_coords[key] = [getattr(i, 'coord', None)
                                        for i in value]
            else:
                self.__init_text_coords[key] = getattr(value, 'coord', None)
        # Finally resize and reposition elements.
        self._view_resize()
        # Update canvas
        self.top.update_idletasks()

    @state.not_state('is_preview')
    def _view_update(self, mode='all'):
        """
        Update canvas view.

        Call each *Stack._update_text()* then *_update_text()* to update
        canvas labels. Update text on status bar and states on menubar.
        """
        for stack in self.allstacks:
            stack._update_text()
        self._update_text()
        kwargs = {}
        if mode in ['all', 'move']:
            kwargs['moves'] = (self.move_index, self.move_total,)
        if mode in ['all', 'seed']:
            kwargs['gamenumber'] = '#{0}'.format(self.random),
        if mode in ['all', 'stat']:
            kwargs['stats'] = STAT_DB.basic(CONF_OPT['player'], self['id'])
        update_console(**kwargs)
        self.top.event_generate('<<update_menus>>')

    @property
    def _view_scale(self):
        """
        Current scale factor for view.

        .. \_view_scale() -> None
        """
        if CONF_OPT['scale_auto']:
            if self.canvas.winfo_ismapped():  # Apparent canvas size.
                size = self.canvas._cinfo_size
            else:    # No apparent canvas size... use prior geometry.
                size = CONF_OPT['wm_geometry']
            # Find scale factor
            factor = [float(s) / i for s, i in zip(size, self.__init_view_size)]
            if CONF_OPT['scale_keep']:
                factor = [min(factor)] * 2
        else:
            factor = CONF_OPT['scale']
        return Scale(*factor)

    def _view_resize(self):
        """
        Resize game canvas and elements.

        .. \_view_resize() -> None
        """
        self._hide_stack_tip()
        scale = self._view_scale
        LOGGER.debug('resize_view: %s x %s', scale.xscale, scale.yscale)

        ancestor = super(Game, self)
        if hasattr(ancestor, '_view_resize'):
            ancestor._view_resize()

        # 1) scale and position piles
        for pile in self.allstacks:
            pile.resize(scale.x, scale.y)
            pile.updatePositions()

        # 2) position texts
        init_coords = self.__init_text_coords
        for key in ('info', 'help', 'misc', 'score', 'base_rank'):
            coord = init_coords[key]
            if coord:
                self.canvas.coords(self.texts[key], *coord._scale(scale))
        for item, coord in zip(self.texts['list'], init_coords['list']):
            self.canvas.coords(item, *coord._scale(scale))

    @state.not_state('is_preview')
    def _play_sound(self, name, priority=-1, loop=False):
        """
        Play sound effect.

        .. play_sound(name:str[, priority:int=-1, loop:bool=False]) -> None

        *Parameters*:
          name : {str}
            name of sound to play
          priority : {int} -1
            sound importance
          loop : {int} False
            sound repeats
        """
        if CONF_OPT['animate_speed'] and name == 'deal':
            self.canvas.update_idletasks()
        AUDIO_DEV.play(name, priority=priority, loop=loop)

    def _stop_sound(self):
        """
        Stop sound effect.

        .. \_stop_sound() -> None
        """
        AUDIO_DEV.stop()

    def _create_game(self, **kws):
        """
        Create game format.

        Place piles on canvas based on current form and group attributes for
        common talon, wastes, rows, foundations, and reserves.

        .. _create_game([*layout form keywords*]) -> Layout

        *Parameters:*
          All keywords are passed to layout attribute create method.

        *Variables:*
          _layout_ : {Layout}
            layout to call to get coords of stacks.
          _row_ : {Pile}
            stacks used for building cards. Appends to *s['rows']*
          _reserve_ : {Pile}
            stacks used for holding cards. Appends to *s['reserve']*
          _talon_ : {Pile}
            stack used for dealing cards. Appends to *s['talon']*
          _waste_ | _wastes_ : {Pile}
            stack used for holding talon cards. Plural handles common auxilary
            wastes with single main waste. Appends to *s['waste']*
          _foundation_ | _foundations_ : {Pile}
            stack(s) used for finishing cards. Plural handles common split
            finish foundations. (eg 1 set build up and 1 set build down).
            Appends to *s['foundations']*
        """
        form_ = getattr(self, '_layout_', None)
        if form_ is None:
            raise NotImplementedError
        form, pile = form_(self, **kws), self.s
        form.create(**kws)
        self.table_size = Size(form.size[0], form.size[1])
        kws = {'game': self}
        # Set pile type for single talon.
        cpile, key = getattr(self, '_talon_', None), 'talon'
        assert cpile is not None
        pile[key] = cpile(form.s[key].x, form.s[key].y, **kws)
        # Set pile type sequence for waste. 1st use all but last. 2nd last.
        cpile, key = getattr(self, '_wastes_', None), 'wastes'
        if isinstance(cpile, (list, tuple,)):
            wastes = getattr(self, '_waste_', None)
            assert wastes is None and len(cpile) == 2
            last = len(form.s['wastes']) - 1
            pile[key] = [wastes[int(last == i)](p.x, p.y, **kws)
                            for i, p in enumerate(form.s[key])]
        # Set pile type sequence for foundation. Single suit set per.
        cpile, key = getattr(self, '_foundations_', None), 'foundations'
        if isinstance(cpile, (list, tuple,)):
            assert (getattr(self, '_foundation_', None) is None and
                    len(form.s[key]) / len(cpile) == form.NSUIT)
            pile[key] = [cpile[i // form.NSUIT](p.x, p.y, suit=p.suit, **kws)
                            for i, p in enumerate(form.s[key])]
        # Set pile type for wastes, foundations, rows, and reserves.
        for key in ['wastes', 'foundations', 'rows', 'reserves']:
            cpile = getattr(self, '_{}_'.format(key[:-1]), None) # singular form
            if cpile is not None:
                if not key == 'foundations':
                    pile[key] = [cpile(p.x, p.y, **kws) for p in form.s[key]]
                else:  # Set foundation suit.
                    pile[key] = [cpile(p.x, p.y, suit=p.suit, **kws)
                                  for p in form.s[key]]
        form.defaultAll()
        return form

    @state.not_state('is_preview')
    def _update_text(self):
        """
        Up canvas texts for current.

        Stub to update text items from primarily game texts not pile texts.

        ..\_update_text(self)
        """
        pass

    @property
    def score(self):
        """
        Score of current game.

        Stub to score a game. Scores in patience games don't always make sense.

        .. score -> int
        """
        return 0

    @property
    def _highlight_piles(self):
        """
        Return piles for highlight possible move.

        Use group hp stacks with more than 2 cards for card matching. Else none.

        .. \__highlight_piles -> seq(seq(seq(Pile), int) ...)
        """
        return ((self.sg['hp_stacks'], 2,),) if self.sg['hp_stacks'] else ()

    def _timer_start(self):
        """
        Start timer and reset status time for current game.

        .. \_timer_start() -> None

        *Variables:*
           play_timer: {Event Timer} - set callback for timer update after
             TIMER_UPDATE.
        """
        if not self.top:
            return
        update_console(time=None)
        self._timer_stop()
        self._timer.adjust()
        self.play_timer = self.top.after(TIMER_UPDATE, self._timer_update)

    def _timer_stop(self):
        """
        Stop timer for current game.

        .. \_timer_stop() -> None.

        *Variables:*
           play_timer: {Event Timer} - unset callback timer.
        """
        if not self.top:
            return
        if hasattr(self, 'play_timer') and self.play_timer:
            self.top.after_cancel(self.play_timer)
            self.play_timer = None
            self._timer_update(after=False)

    @state.not_state('is_finish', 'is_pause')
    def _timer_update(self, after=True):
        """
        Update timer and status bar for current game.

        When given do_after, set next callback to call self after TIMER_UPDATE.
        Calls update status method to update time value regardless.

        .. \_timer_update(after:bool=True)

        *Parameters*:
          after : {bool} True
            whether callback is set.

        *Variables:*
           play_timer: {Event Timer} - set callback for self after TIMER_UPDATE.
        """
        timer = str(self._timer)
        if not self.top:
            return
        if after:
            self.play_timer = self.top.after(TIMER_UPDATE, self._timer_update)
        if not self.is_preview:
            update_console(time=timer)

    @state.not_state('is_finish')
    def _timer_pause(self, pause=True):
        """
        Enter or Exit Pause.

          - Enter: hide canvas game items and update timer.
          - Exit: unhide canvas game items and adjust timer before update.

        .. _timer_pause(pause:bool=True) -> None

        *Variables:*
          pause: {boolean}
            pause or unpase based on current state.
        """
        if self.is_pause == pause:
            return
        self.demo_stop()   # Stop demo_mode
        self.is_pause = pause
        if self.is_pause:  # Pause timer and game
            self.top.event_generate('<<update_menus>>')
            self._timer_update(after=False)
            self.canvas.hide('game')
            self.canvas.set_overlay('pause')
        else:              # Resume timer and game
            self._timer.adjust()
            self._timer_update()
            self.canvas.set_overlay()
            self.canvas.show('game')

    @property
    def is_complete(self):
        """
        Return game complete.

        Default s[foundations] contain all cards.

        .. is_complete -> bool
        """
        return bool(sum(len(s.cards)
                    for s in self.s['foundations']) == self['ncards'])

    @property
    def talon(self):
        """
        Return talon piles for dealing cards.

        .. talon -> Pile
        """
        return self.s['talon']

    def _autopiles(self, event=None):
        """
        Return piles for auto or quick move.

        Return (autoflip, autodrop, quickplay). Default *sg.dropstacks \* 3*.
        Overide to adjust behavior of auto and quick move behavior.

        .. \_autopiles(self, event=None) -> seq(seq(Pile ...), seq(Pile ...),
             seq(Pile ...))
        """
        return ([self.sg['dropstacks']] * 3)


    def _hide_stack_tip(self):
        """
        Remove pile tip items from canvas.

        .. \_hide_stack_tip() -> None
        """
        from ..viewlib.tooltip import PileToolTips
        return PileToolTips.delete()